﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace Hellquest
{
    #region types
    struct TextureInfo
    {
        public Texture2D Texture;
        public Rectangle? SourceRectangle;
        public Rectangle DestinationRectangle;
        public Color Color;
        public float Rotation;
        public Vector2 Center;
        public float Depth;

        public TextureInfo(Texture2D _texture, Rectangle _destRect, Rectangle? _sourceRect, Color _color, float _rotation, bool _centered, float _depth)
        {
            Texture = _texture;
            SourceRectangle = _sourceRect;
            DestinationRectangle = _destRect;
            Color = _color;
            Rotation = _rotation;
            Depth = _depth;

            if (_centered)
            {
                if (_sourceRect.HasValue)
                    Center = new Vector2(_sourceRect.Value.Center.X - _sourceRect.Value.X, _sourceRect.Value.Center.Y - _sourceRect.Value.Y);
                else
                    Center = new Vector2(Texture.Bounds.Center.X, Texture.Bounds.Center.Y);
            }
            else
                Center = Vector2.Zero;
        }
    }

    public enum TextAlignement
    {
        Centered,
        Right,
        Left
    }
    struct Text
    {
        public string String;
        public SpriteFont Font;
        public Vector2 Position;
        public Color Color;
        public float Depth;

        public static Text SetValues(string _text, SpriteFont _font, Vector2 _position, Color _color, float _depth = 0)
        {
            Text retText = new Text();
            retText.String = _text;
            retText.Font = _font;
            retText.Position = _position;
            retText.Color = _color;
            retText.Depth = _depth;
            return retText;
        }
    }

    struct LineInfo
    {
        public Vector2 Start;
        public Vector2 End;
        public Color Color;
        public float Width;
        public LineInfo(Vector2 _start, Vector2 _end, float _width, Color _color)
        {
            Start = _start;
            End = _end;
            Color = _color;
            Width = _width;
        }
    }
    #endregion

    public class Graphics : DrawableGameComponent
    {
        #region fields
        private const float FPS_INTERVALL = 0.2f;

        private List<Sprite> spritesToDraw;
        private List<Text> textToDraw;
        private List<TextureInfo> particlesToDraw;
        private List<TextureInfo> ingameTexturesToDraw;
        private List<TextureInfo> guiTexturesToDraw;
        private List<LineInfo> linesToDraw;

        private Dictionary<string, Effect> effects;

        private Vector2 currentResolution;

        public Texture2D emptyTexture;
        private Dictionary<string, Texture2D> textures;

        private Dictionary<string, SpriteFont> fonts;

        private GraphicsDeviceManager graphics;
        private SpriteBatch spriteBatch;

        private RenderTarget2D rtWorld;
        private RenderTarget2D rtParticles;
        private RenderTarget2D rtWorldDepth;
        private RenderTarget2D rtParticlesDepth;
        #endregion

        #region properties
        private static Graphics mInstance;
        public static Graphics Instance { get { return mInstance; } }

        public Camera MainCamera;
        public Vector2 Resolution { get { return currentResolution; } }
        public Rectangle ScreenRectangle { get { return new Rectangle(0, 0, (int)Resolution.X, (int)Resolution.Y); } }
        public Color DefaultColor { get; set; }
        public bool ShowMouse
        {
            set
            {
                Game.IsMouseVisible = value;
            }
            get
            {
                return Game.IsMouseVisible;
            }
        }
        public bool VSync
        {
            get
            {
                if (graphics != null)
                    return graphics.SynchronizeWithVerticalRetrace;
                else
                    return false;
            }
            set
            {
                if (graphics != null)
                {
                    graphics.SynchronizeWithVerticalRetrace = value;
                    graphics.ApplyChanges();
                }
            }
        }
        public bool ShowFPS;
        public int FPS { get { return fps; } }
        #endregion

        private SpriteFont fpsFont;
        private int frameCount;
        private int fps;
        private float lastSecond;

        public Graphics(Game _game)
            : base(_game)
        {
            mInstance = this;
            spritesToDraw = new List<Sprite>();
            particlesToDraw = new List<TextureInfo>();
            ingameTexturesToDraw = new List<TextureInfo>();
            guiTexturesToDraw = new List<TextureInfo>();
            textToDraw = new List<Text>();
            linesToDraw = new List<LineInfo>();
            DefaultColor = Color.CornflowerBlue;
            textures = new Dictionary<string, Texture2D>();
            effects = new Dictionary<string, Effect>();
            fonts = new Dictionary<string, SpriteFont>();


            graphics = new GraphicsDeviceManager(_game);
            graphics.PreparingDeviceSettings += this.graphics_PreparingDeviceSettings;

            VSync = false;
            ShowFPS = true;
            currentResolution = new Vector2();

            frameCount = 0;
        }

        /// <summary>
        /// Set the current screen resolution and fullscreen flag
        /// </summary>
        public void SetResolution(int _x, int _y, bool _fullScreen)
        {
            graphics.PreferredBackBufferWidth = _x;
            graphics.PreferredBackBufferHeight = _y;

            currentResolution.X = _x;
            currentResolution.Y = _y;

            graphics.IsFullScreen = _fullScreen;
            graphics.ApplyChanges();
        }

        #region Sprite Handling
        public void AddSprite(Sprite _sprite)
        {
            spritesToDraw.Add(_sprite);
        }

        public void RemoveSprite(Sprite _sprite)
        {
            spritesToDraw.Remove(_sprite);
        }

        #endregion

        #region public draw methods
        public void DrawParticle(Texture2D _texture, Vector2 _position, Rectangle? _sourceRectangle, Color _color, Vector2 _scale, float _rotation = 0f, bool _centered = false, float _depth = 0)
        {
            Rectangle destRect;
            if (_sourceRectangle.HasValue)
            {
                destRect = new Rectangle((int)_position.X, (int)_position.Y, (int)(_sourceRectangle.Value.Width * _scale.X), (int)(_sourceRectangle.Value.Height * _scale.Y));
            }
            else
            {
                destRect = new Rectangle((int)_position.X, (int)_position.Y, (int)(_texture.Width * _scale.X), (int)(_texture.Height * _scale.Y));
            }
            if (MainCamera.WorldToScreen(destRect).Intersects(ScreenRectangle))
                particlesToDraw.Add(new TextureInfo(_texture, destRect, _sourceRectangle, _color, _rotation, _centered, _depth));
        }

        /// <summary>
        /// Draws an ingame texture which position is given in world coordinates.
        /// </summary>
        public void DrawIngameTexture(Texture2D _texture, Rectangle _destinationRectangle, Rectangle? _sourceRectangle, Color _color, float _rotation = 0f, bool _centered = false, float _depth = 0)
        {
            ingameTexturesToDraw.Add(new TextureInfo(_texture, _destinationRectangle, _sourceRectangle, _color, _rotation, _centered, _depth));
        }

        /// <summary>
        /// Draws an ingame texture which position is given in world coordinates.
        /// </summary>
        public void DrawIngameTexture(Texture2D _texture, Vector2 _position, Rectangle? _sourceRectangle, Color _color, float _rotation = 0f, bool _centered = false, float _depth = 0)
        {
            Rectangle _destinationRectangle = new Rectangle((int)Math.Floor(_position.X), (int)Math.Floor(_position.Y), (int)_texture.Width, (int)_texture.Height);
            ingameTexturesToDraw.Add(new TextureInfo(_texture, _destinationRectangle, _sourceRectangle, _color, _rotation, _centered, _depth));
        }

        public void DrawIngameTexture(Texture2D _texture, Vector2 _position, Rectangle? _sourceRectangle, Color _color, Vector2 _scale, float _rotation = 0f, bool _centered = false, float _depth = 0)
        {
            Rectangle _destinationRectangle = new Rectangle((int)Math.Floor(_position.X), (int)Math.Floor(_position.Y), (int)(_texture.Width * _scale.X), (int)(_texture.Height * _scale.Y));
            ingameTexturesToDraw.Add(new TextureInfo(_texture,
                                     _destinationRectangle,
                                     _sourceRectangle, _color, _rotation, _centered, _depth));
        }

        /// <summary>
        /// Draws a screenspace GUI string.
        /// </summary>
        public void DrawString(SpriteFont _font, string _text, Vector2 _position, Color _color, TextAlignement _alignement = TextAlignement.Left, float _depth = 0)
        {
            Vector2 offset = Vector2.Zero;
            switch (_alignement)
            {
                case TextAlignement.Left: break;
                case TextAlignement.Centered: offset = _font.MeasureString(_text) / 2; break;
                case TextAlignement.Right: offset = _font.MeasureString(_text); break;
            }
            textToDraw.Add(Text.SetValues(_text, _font, _position - offset, _color, _depth));
        }

        /// <summary>
        ///  Draws a screenspace GUI string.
        /// </summary>
        public void DrawString(string _text, Vector2 _position, Color _color, TextAlignement _alignement = TextAlignement.Left, float _depth = 0)
        {
            DrawString(GetFont(@"SmallFont"), _text, _position, _color, _alignement, _depth);
        }

        public void DrawLine(Vector2 _start, Vector2 _end, float _width, Color _color, float _depth = 0)
        {
            linesToDraw.Add(new LineInfo(_start, _end, _width, _color));
        }

        /// <summary>
        /// Draws a screenspace GUI Texture
        /// </summary>
        public void DrawGUITexture(Texture2D _texture, Rectangle _destinationRectangle, Rectangle? _sourceRectangle, Color _color, float _depth = 0)
        {
            guiTexturesToDraw.Add(new TextureInfo(_texture, _destinationRectangle, _sourceRectangle, _color, 0, false, _depth));
        }

        /// <summary>
        /// Draws a screenspace GUI Texture
        /// </summary>
        public void DrawGUITexture(Texture2D _texture, Vector2 _position, Rectangle? _sourceRectangle, Color _color, float _depth = 0)
        {
            guiTexturesToDraw.Add(new TextureInfo(_texture, new Rectangle((int)_position.X, (int)_position.Y, (int)_texture.Width, (int)_texture.Height),
                                  _sourceRectangle,
                                  _color,
                                  0,
                                  false,
                                  _depth));
        }

        public void DrawGizmo(Rectangle _rect)
        {
            if (Properties.DRAW_GIZMOS)
            {
                Vector2[] points = new Vector2[4];
                points[0] = new Vector2(_rect.X, _rect.Y);
                points[1] = new Vector2(_rect.X, _rect.Y + _rect.Height);
                points[2] = new Vector2(_rect.X + _rect.Width, _rect.Y + _rect.Height);
                points[3] = new Vector2(_rect.X + _rect.Width, _rect.Y);

                DrawGizmo(new List<Vector2>(points));
            }
        }

        public void DrawGizmo(List<Vector2> _points)
        {
            if (Properties.DRAW_GIZMOS)
            {
                for (int i = 0; i < _points.Count; i++)
                {
                    DrawLine(_points[i], _points[(i + 1) % _points.Count], 1, Color.Yellow);
                }
            }

        }
        #endregion

        #region Content Management
        /// <summary>
        /// Gets a Texture from the Texture Database
        /// </summary>
        public Texture2D GetTexture(string _path)
        {
            if (!textures.ContainsKey(_path))
                textures[_path] = Game.Content.Load<Texture2D>(_path);
            return textures[_path];
        }

        /// <summary>
        /// Gets a Effect from the Effect Database
        /// </summary>
        public Effect GetEffect(string _path)
        {
            if (!effects.ContainsKey(_path))
                effects[_path] = Game.Content.Load<Effect>(_path);
            return effects[_path];
        }

        /// <summary>
        /// Gets a fonst from the Font Database
        /// </summary>
        public SpriteFont GetFont(string _path)
        {
            if (!fonts.ContainsKey(_path))
                fonts[_path] = Game.Content.Load<SpriteFont>(_path);
            return fonts[_path];
        }
        #endregion

        #region DrawableGameComponent Method Overrides


        //Enable PerHUD support (disable for final release)
        void graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            foreach (GraphicsAdapter curAdapter in GraphicsAdapter.Adapters)
            {
                if (curAdapter.Description.Contains("PerfHUD"))
                {
                    e.GraphicsDeviceInformation.Adapter = curAdapter;
                    GraphicsAdapter.UseReferenceDevice = true;
                    break;
                }
            }
            return;
        }

        //Hack to fix minimizing reset bug
        void GraphicsDevice_DeviceReset(object sender, EventArgs e)
        {
            GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
            GraphicsDevice.SamplerStates[1] = SamplerState.PointClamp;
            GraphicsDevice.SamplerStates[2] = SamplerState.PointClamp;
            GraphicsDevice.SamplerStates[3] = SamplerState.PointClamp;
            GraphicsDevice.SamplerStates[4] = SamplerState.PointClamp;
            GraphicsDevice.SamplerStates[5] = SamplerState.PointClamp;
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            emptyTexture = new Texture2D(GraphicsDevice, 1, 1, false, SurfaceFormat.Color);
            emptyTexture.SetData(new[] { Color.White });

            base.LoadContent();
        }

        public override void Initialize()
        {
            base.Initialize();
            rtWorld = new RenderTarget2D(GraphicsDevice, (int)Resolution.X, (int)Resolution.Y, false, SurfaceFormat.Color, DepthFormat.Depth24);
            rtParticles = new RenderTarget2D(GraphicsDevice, (int)Resolution.X, (int)Resolution.Y);
            rtWorldDepth = new RenderTarget2D(GraphicsDevice, (int)Resolution.X, (int)Resolution.Y);
            rtParticlesDepth = new RenderTarget2D(GraphicsDevice, (int)Resolution.X, (int)Resolution.Y);
            fpsFont = Game.Content.Load<SpriteFont>(@"SmallFont");
            GraphicsDevice.DeviceReset += new EventHandler<EventArgs>(GraphicsDevice_DeviceReset);
        }



        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(ClearOptions.DepthBuffer | ClearOptions.Target, Color.Transparent, 1f, 0);
            //If ingame
            if (MainCamera != null)
            {
                RenderParticles();
                RenderWorld();

                GraphicsDevice.SetRenderTarget(null);
              
                spriteBatch.Begin(SpriteSortMode.Deferred,
                                        BlendState.AlphaBlend,
                                        SamplerState.PointClamp,
                                        DepthStencilState.Default,
                                        null,
                                        null);

                spriteBatch.Draw(rtWorld, Vector2.Zero, Color.White);
                spriteBatch.End();

                //Combine Particles with the rest of the image
                spriteBatch.Begin(SpriteSortMode.Immediate,
                                BlendState.AlphaBlend,
                                SamplerState.LinearClamp,
                                null,
                                null,
                                GetEffect(@"Effects\ParticleCombine"));
                GetEffect(@"Effects\ParticleCombine").Parameters["worldDepthTex"].SetValue(rtWorldDepth);
                GetEffect(@"Effects\ParticleCombine").Parameters["particleDepthTex"].SetValue(rtParticlesDepth);
                spriteBatch.Draw(rtParticles, Vector2.Zero, Color.White);
                spriteBatch.End();
            }
            else
            {
                GraphicsDevice.Clear(DefaultColor);
            }

            RenderGUI();

            //Calculate/Display System stats
            frameCount++;
            lastSecond += (float)gameTime.ElapsedGameTime.TotalSeconds;
            if (lastSecond >= FPS_INTERVALL)
            {
                fps = (int)(frameCount * (1f / FPS_INTERVALL));
                frameCount = 0;
                lastSecond = 0;
            }

            if (ShowFPS)
            {
                spriteBatch.DrawString(fpsFont, "FPS: " + fps.ToString() + "(" + ((1f / fps) * 1000).ToString() + " ms)", Resolution - new Vector2(200, 75), Color.White);
                //spriteBatch.DrawString(fpsFont, "Particle Count: " + ParticleEngine.Instance.ParticleCount.ToString(), Resolution - new Vector2(200, 25), Color.White);
            }

            spriteBatch.End();

            //Clear frame updated lists
            particlesToDraw.Clear();
            textToDraw.Clear();
            guiTexturesToDraw.Clear();
            ingameTexturesToDraw.Clear();
            linesToDraw.Clear();

            base.Draw(gameTime);
        }

        private void RenderParticles()
        {
            //Particle Rendering
            GraphicsDevice.SetRenderTarget(rtParticles);
            GraphicsDevice.Clear(Color.Transparent);
            spriteBatch.Begin(SpriteSortMode.Deferred,
                                      BlendState.Additive,
                                      SamplerState.LinearClamp,
                                      null,
                                      null,
                                      null,
                                      MainCamera.Transformation);

            foreach (TextureInfo texInfo in particlesToDraw)
            {
                spriteBatch.Draw(texInfo.Texture,
                    texInfo.DestinationRectangle,
                    texInfo.SourceRectangle,
                    texInfo.Color,
                    texInfo.Rotation,
                    texInfo.Center,
                    SpriteEffects.None,
                    texInfo.Depth);
            }
            spriteBatch.End();


            GraphicsDevice.SetRenderTarget(rtParticlesDepth);
            GraphicsDevice.Clear(Color.White);
            spriteBatch.Begin(SpriteSortMode.Deferred,
                                      BlendState.NonPremultiplied,
                                      SamplerState.LinearClamp,
                                      null,
                                      null,
                                      GetEffect(@"Effects\DepthTestPartilces"),
                                      MainCamera.Transformation);
            GetEffect(@"Effects\DepthTestPartilces").Parameters["offset"].SetValue(MainCamera.Position);
            GetEffect(@"Effects\DepthTestPartilces").Parameters["Viewport"].SetValue(Resolution);
            foreach (TextureInfo texInfo in particlesToDraw)
            {
                spriteBatch.Draw(texInfo.Texture,
                    texInfo.DestinationRectangle,
                    texInfo.SourceRectangle,
                    Color.White,
                    texInfo.Rotation,
                    texInfo.Center,
                    SpriteEffects.None,
                    texInfo.Depth);
            }
            spriteBatch.End();
        }


        private void RenderWorld()
        {
            //World Rendering
            GraphicsDevice.SetRenderTarget(rtWorld);
            GraphicsDevice.Clear(ClearOptions.DepthBuffer | ClearOptions.Target, MainCamera.ClearColor, 1f, 0);

            //Draw the Map and all other ingame textures
            spriteBatch.Begin(SpriteSortMode.Deferred,
                              BlendState.AlphaBlend,
                              SamplerState.PointClamp,
                              DepthStencilState.Default,
                              null,
                              null,
                              MainCamera.Transformation);

            foreach (TextureInfo texInfo in ingameTexturesToDraw)
            {
                spriteBatch.Draw(texInfo.Texture,
                    texInfo.DestinationRectangle,
                    texInfo.SourceRectangle,
                    texInfo.Color,
                    texInfo.Rotation,
                    texInfo.Center,
                    SpriteEffects.None,
                    texInfo.Depth);
            }

            spriteBatch.End();

            // Draw all sprites
            spriteBatch.Begin(SpriteSortMode.BackToFront,
                              BlendState.AlphaBlend,
                              SamplerState.PointClamp,
                              DepthStencilState.Default,
                              null,
                              null,
                              MainCamera.Transformation);
            foreach (Sprite sprite in spritesToDraw)
            {
                if (sprite.Texture != null && new Rectangle((int)(sprite.Position.X - sprite.Origin.X), (int)(sprite.Position.Y - sprite.Origin.Y), sprite.Width, sprite.Height).Intersects(MainCamera.ScreenToWorld(ScreenRectangle)))
                {
                    spriteBatch.Draw(sprite.Texture,
                                        sprite.Position - sprite.drawingOffset,
                                        sprite.SourceRectangle,
                                        Color.White,
                                        sprite.gameObject.transform.Rotation,
                                        sprite.Origin,
                                        sprite.Scale,
                                        sprite.Effects,
                                        sprite.Depth);
                }

            }
            spriteBatch.End();

            GraphicsDevice.SetRenderTarget(rtWorldDepth);
            GraphicsDevice.Clear(Color.White);
            spriteBatch.Begin(SpriteSortMode.BackToFront,
                              BlendState.NonPremultiplied,
                              SamplerState.PointClamp,
                              DepthStencilState.Default,
                              null,
                              GetEffect(@"Effects\DepthTestWorld"),
                              MainCamera.Transformation);

            GetEffect(@"Effects\DepthTestWorld").Parameters["offset"].SetValue(MainCamera.Position);
            GetEffect(@"Effects\DepthTestWorld").Parameters["Viewport"].SetValue(Resolution);
            foreach (Sprite sprite in spritesToDraw)
            {
                if (sprite.Texture != null && new Rectangle((int)sprite.Position.X, (int)sprite.Position.Y, sprite.Width, sprite.Height).Intersects(MainCamera.ScreenToWorld(ScreenRectangle)))
                {
                    spriteBatch.Draw(sprite.Texture,
                                        new Vector2(sprite.gameObject.transform.WorldPosition.X - sprite.drawingOffset.X,
                                                    sprite.gameObject.transform.WorldPosition.Y - sprite.drawingOffset.Y),
                                        sprite.SourceRectangle,
                                        Color.White,
                                        sprite.gameObject.transform.Rotation,
                                        sprite.Origin,
                                        sprite.Scale,
                                        sprite.Effects,
                                        sprite.Depth);
                }

            }
            spriteBatch.End();
        }

        private void RenderGUI()
        {
            spriteBatch.Begin(SpriteSortMode.BackToFront,
                                  BlendState.AlphaBlend,
                                  SamplerState.PointClamp,
                                  null,
                                  null,
                                  null);

            foreach (TextureInfo texInfo in guiTexturesToDraw)
            {
                spriteBatch.Draw(texInfo.Texture, texInfo.DestinationRectangle, texInfo.SourceRectangle, texInfo.Color, texInfo.Rotation, Vector2.Zero, SpriteEffects.None, texInfo.Depth);
            }

            foreach (Text text in textToDraw)
            {
                spriteBatch.DrawString(text.Font, text.String, text.Position, text.Color);
            }

            foreach (LineInfo line in linesToDraw)
            {
                Vector2 lineVector = line.End - line.Start;
                float angle = (float)Math.Atan2(lineVector.Y, lineVector.X);
                float length = lineVector.Length();
                Vector2 orthogonal = new Vector2(-lineVector.Y, lineVector.X);
                orthogonal.Normalize();

                Vector2 startPoint = line.Start - orthogonal * (line.Width / 2);

                spriteBatch.Draw(emptyTexture, startPoint, null, line.Color,
                       angle, Vector2.Zero, new Vector2(length, line.Width),
                       SpriteEffects.None, 0);
            }
        }
        #endregion
    }
}
